package lock.redis;

import redis.clients.jedis.Jedis;

import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;

import static lock.redis.Constant.LOCK_TIMEOUT_SECONDS;
import static lock.redis.Constant.WATCH_DOG_INTERVAL_SECONDS;

/**
 * 可重入单redis锁
 */
public class ReentrantSingleRedisLock implements RedisLock {
    Jedis client;
    String lockValue;

    String lockScript = """
            if (redis.call('exists', KEYS[1]) == 0) then
                redis.call('hset', KEYS[1], 'owner', ARGV[1])
                redis.call('hset', KEYS[1], 'count', 1)
                redis.call('pexpire', KEYS[1], ARGV[2])
                return 1
            elseif (redis.call('hget', KEYS[1], 'owner') == ARGV[1]) then
                redis.call('hincrby', KEYS[1], 'count', 1)
                redis.call('pexpire', KEYS[1], ARGV[2])
                return 1
            else
                return 0
            end
            """;
    String unLockScript = """
            if redis.call('hget', KEYS[1], 'owner') == ARGV[1] then
                local count = tonumber(redis.call('hincrby', KEYS[1], 'count', -1))
                if (count <= 0) then
                    redis.call('del', KEYS[1])
                    return 0
                else
                    return count
                end
            else
                return -1
            end
            """;

    String watchDogScript = """
            if (redis.call("exists", KEYS[1]) == 1) then
                if (redis.call("hget", KEYS[1], "owner") == ARGV[1]) then
                    redis.call("pexpire", KEYS[1], ARGV[2])
                    return 1
                else
                    return 0
                end
            else
                return -1
            end
            """;

    AtomicInteger lockCount = new AtomicInteger(0);

    public ReentrantSingleRedisLock(Jedis client) {
        this.client = client;
        genLockKey();
    }

    void genLockKey() {
        this.lockValue = UUID.randomUUID() + ":" + Thread.currentThread().threadId();
    }

    public boolean lock(String key) {
        return lock(key, LOCK_TIMEOUT_SECONDS);
    }

    public boolean lock(String key, long timeOut) {
        Object result = client.eval(lockScript, Collections.singletonList(key), List.of(lockValue, String.valueOf(LOCK_TIMEOUT_SECONDS * 1000L)));
        boolean ok = result != null && (long) result == 1;
        if (ok && lockCount.compareAndSet(0, 1)) {
            startWatchDog(key);
        }
        return ok;
    }

    private void startWatchDog(String key) {
        new Thread(() -> {
            while (lockCount.get() > 0) {
                try {
                    Thread.sleep(WATCH_DOG_INTERVAL_SECONDS * 1000L);
                    client.eval(watchDogScript, Collections.singletonList(key), List.of(lockValue, String.valueOf(LOCK_TIMEOUT_SECONDS * 1000L)));
                    // TODO 考虑续约发现锁被其他线程持有
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }).start();
    }

    public boolean unlock(String key) {
        Object result = client.eval(unLockScript, Collections.singletonList(key), Collections.singletonList(lockValue));
        boolean ok = result != null && (long) result != -1;
        if (ok) {
            lockCount.decrementAndGet();
        }
        return ok;
    }

    @Override
    public void close() {
        client.close();
    }
}
